'''
Created on Apr 17, 2009

@author: caesar
'''

import sys, time
import telnetlib
import state


class MuninFetcherException (Exception):
  pass

class MuninFetcherNoValue (MuninFetcherException):
  pass

class MuninFetcherNodeWentOffline (MuninFetcherException):
  pass

class MuninFetcher (object):
  """
  the main facility to interact with munin node via telnet
  """
  def __init__(self, tn, storage, echo = False):
    """tn and storage are here for easy unit-testing"""
    self.storage = storage
    self.echo = echo
    self.tn = tn
    tn.read_until("\n") #skip the header
    self.modules = self._list_modules()

  def _process_property (self, data, handler):
    try:
      current_time = time.mktime(time.localtime())
      try:
        type = data['type']
      except KeyError:
        type = 'GAUGE'
      
      value = None
      
      try:
        value_number = state.to_number(data['value'])
      except KeyError:
        raise MuninFetcherNoValue
      
      if type in ('COUNTER', 'DERIVE', 'ABSOLUTE'):
        #get last value
        try:
          last_time, last_value = handler.get()
          if type == 'ABSOLUTE':
            last_value = 0 # absolute counters are dropped to zero after each check
            
          data['value'] = (value_number - last_value) / (current_time - last_time)
        except KeyError:
          pass
         
        handler.set ((current_time, value_number))

      elif type == 'GAUGE':
        data['value'] = value_number
        
    except MuninFetcherNoValue:
      data['value'] = None
    
    return state.MuninPropertyState(data)

  def _process_module(self, module):
    raw_options, raw_properties = self.fetch(module)
    
    module_state = state.MuninModuleState(raw_options)
    for property, data in raw_properties.iteritems():
      try:
        property_state = self._process_property(data, self.storage.get_handler(module, property))
        module_state.states[property] = property_state
      except state.MuninPropertyError, e:
        pass # @todo: log the error 
    return module_state


  def _list_modules(self):
    """get the list of modules
    @todo: validation! should wrap any possible telnet exception. probably.
    """
    out = self._run_command('list', "\n")
    return out.rstrip().split(" ")
    
  def _run_command(self, command, termination):
    """
    send the command to the server and get what the server will answer until 
    the termination string will be received.
    
    termination is included in output.
    
    @raise MuninFetcherNodeWentOffline: if there was any connection problems while interacting with the 
    server 
    """
    try:
      if self.echo:
        print ">", command
      self.tn.write(command+"\n")
      out = self.tn.read_until(termination)
      if self.echo:
        print out
      return out
    except EOFError, error:
      raise MuninFetcherNodeWentOffline('node went offline on command: '+command)
  


  def get_node_state(self):
    """
    @todo: retry connection if bode went offline. It happens sometimes. need max_retry and retry delay configurable variables 
    @return: MuninNodeState instance
    """
    node_state = state.MuninNodeState(True)

    for module in self.modules:
      try:
        module_state = self._process_module(module)
      except MuninFetcherNodeWentOffline, problem:
        node_state.accessible = False
        node_state.message = problem.__str__()
          
      node_state.states[module] = module_state
      
    return node_state
          
  
  def fetch(self, module):
    """
    do both fetch and config and return as single parsed data set
    
    will return tuple in following form:
    (
      {'option1': 'value1', ...}, 
      {'property1': 
        {'field': 'value1',
         'field2': 'value2',
         ...
        },
        ...
      }
    ) 
    """
    
    if not module in self.modules:
      raise MuninFetcherException('no module %s for this node' % module)
      
    raw = self._run_command('config '+module, "\n.\n")+self._run_command('fetch '+module, "\n.\n")

    options = {}
    properties = {}
    
    for line in raw.splitlines():
      line = line.strip()
      if line == '.' or len(line) == 0 or line[0] == "#": continue # skip the delimiter, and empty lines, and comments
      
      key, value = line.split(" ", 1)
      
      key   = key.strip()
      value = value.strip()
      
      if key.find(".") == -1:
        options[key] = value
      else:
        field, property = key.split('.', 1)
        if not properties.has_key(field): properties[field] = {}
        properties[field][property] = value
    
    return (options, properties)

class MuninStorage (object):
  """
  Stores the last state and time for properties
  """
  def __init__(self, file):
    """
    needs file-like object with file-like iterator, seek(), truncate(), and write() interface
    """
    self.file = file
    self.parse_data()
  
  def parse_data (self):
    """
    parses self.file's data into {(module, property): (timestamp, value), } dictionary
    """
    self.data = {}
    for line in self.file:
      key, value = line.split(" ", 1)
      module, property = key.split('.')
      time, value = value.split(" ")
      self.data[(module, property)] = (int(state.to_number(time)), state.to_number(value))
          
  def dump_data (self):
    """
    dumps self.data into the file object in the following form:
    module.property time value
    """
    self.file.seek(0)
    for key, value in self.data.iteritems():
      to_add = "%s.%s %s %s\n" % (key[0].__str__(), key[1].__str__(), value[0].__str__(), value[1].__str__())
      self.file.write(to_add)
    return self.file.truncate()
  
  def get_handler(self, module, property):
    return StorageHandler(module, property, self.data)
    
  def __del__ (self):      
    self.dump_data()
    

class StorageHandler:
  def __init__(self, module, property, storage_data):
    self.key = (module, property)
    self.storage_data = storage_data
    
  def get(self):
    return self.storage_data[self.key]
  
  def set(self, new_value):
    self.storage_data[self.key] = new_value
    
